<script lang="ts">
    import { onDestroy } from "svelte";
    import type { ControlPosition, DraggableData, DraggableProps } from "./Draggable";
    import { canDragX, canDragY, createCSSTransform, createDraggableData, getBoundPosition } from "./utils";
    import { useStyle } from "../utils/useStyle";
    import { useClassList } from "../utils/useClassList";
    import DraggableCore from "./DraggableCore.svelte";

    const props: DraggableProps = $props();

    const defaultPosition = props.defaultPosition || { x: 0, y: 0 };
    const store = $state({
        // Whether or not we are currently dragging.
        dragging: false,

        // Whether or not we have been dragged before.
        dragged: false,

        // Current transform x and y.
        x: props.position ? props.position.x : defaultPosition.x,
        y: props.position ? props.position.y : defaultPosition.y,

        prevPropsPosition: { ...props.position },

        // Used for compensating for out-of-bounds drags
        slackX: 0,
        slackY: 0,
    });
    const scale = props.scale || 1;
    const bounds = props.bounds || false;

    const onDragStart = (e: any, coreData: DraggableData) => {
        // Short-circuit if user's callback killed it.
        const shouldStart = props.onstart && props.onstart(e, createDraggableData(store, scale, coreData));
        // Kills start event on core as well, so move handlers are never bound.
        if (shouldStart === false) return false;

        store.dragging = true;
        store.dragged = true;
    };
    const onDrag = (e: any, coreData: DraggableData) => {
        if (!store.dragging) return false;
        const uiData = createDraggableData(store, scale, coreData);

        const newState = {
            x: uiData.x,
            y: uiData.y,
            slackX: 0,
            slackY: 0,
        };

        // Keep within bounds.
        if (bounds) {
            // Save original x and y.
            const { x, y } = newState;

            // Add slack to the values used to calculate bound position. This will ensure that if
            // we start removing slack, the element won't react to it right away until it's been
            // completely removed.
            newState.x += store.slackX;
            newState.y += store.slackY;

            // Get bound position. This will ceil/floor the x and y within the boundaries.
            const [newStateX, newStateY] = getBoundPosition({ bounds, node: coreData.node }, newState.x, newState.y);
            newState.x = newStateX;
            newState.y = newStateY;

            // Recalculate slack by noting how much was shaved by the boundPosition handler.
            newState.slackX = store.slackX + (x - newState.x);
            newState.slackY = store.slackY + (y - newState.y);

            // Update the event we fire to reflect what really happened after bounds took effect.
            uiData.x = newState.x;
            uiData.y = newState.y;
            uiData.deltaX = newState.x - store.x;
            uiData.deltaY = newState.y - store.y;
        }

        // Short-circuit if user's callback killed it.
        const shouldUpdate = props.ondrag && props.ondrag(e, uiData);
        if (shouldUpdate === false) return false;

        store.x = newState.x;
        store.y = newState.y;
        store.slackX = newState.slackX;
        store.slackY = newState.slackY;
    };

    const onDragStop = (e: any, coreData: DraggableData) => {
        if (!store.dragging) return false;

        // Short-circuit if user's callback killed it.
        const shouldContinue = props.onstop && props.onstop(e, createDraggableData(store, scale, coreData));
        if (shouldContinue === false) return false;

        store.dragging = false;
        store.slackX = 0;
        store.slackY = 0;
    };

    onDestroy(() => {
        store.dragging = false;
    });

    const axis = $derived(props.axis || "both");

    const transformOpts = $derived.by(() => ({
        // Set left if horizontal drag is enabled
        x: canDragX(axis) ? store.x : defaultPosition.x,

        // Set top if vertical drag is enabled
        y: canDragY(axis) ? store.y : defaultPosition.y,
    }));

    const style = $derived.by(() => {
        return useStyle(props, { ...createCSSTransform(transformOpts, props.positionOffset) });
    });

    const classList = $derived(
        useClassList(props, "cm-draggable", {
            "cm-draggable-dragging": store.dragging,
            "cm-draggable-dragged": store.dragged,
        })
    );

    // Reset
    export const reset = () => {
        store.x = 0;
        store.y = 0;
    };

    // custom setPosition
    export const setPosition = (pos: ControlPosition) => {
        store.x = pos.x;
        store.y = pos.y;
    };
</script>

<DraggableCore grid={props.grid} class={classList} disabled={props.disabled} handle={props.handle} {scale} {style} onStart={onDragStart} {onDrag} onStop={onDragStop}>
    {@render props.children?.()}
</DraggableCore>
